package homework.qingming.calc;

import homework.qingming.calc.functions.BaseFunction;
import homework.qingming.calc.functions.SqrtFunction;
import homework.qingming.calc.operations.*;
import homework.util.BigDecimalMath;

import java.math.BigDecimal;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.EmptyStackException;
import java.util.HashMap;
import java.util.Stack;

public class Calculator {

    public final Character LEFT_PAR = '(';
    public final Character RIGHT_PAR = ')';
    public final Character FUNC_PARAMS_SEP = ',';

    private final String leftPar = LEFT_PAR.toString();
    private final String rightPar = RIGHT_PAR.toString();
    private final String funcParSep = FUNC_PARAMS_SEP.toString();

    private FormulaScanner fsc;

    private Stack<Executable> oprStack = new Stack<Executable>();
    private Stack<BigDecimal> numStack = new Stack<BigDecimal>();

    private HashMap<Character, BaseOperation> oprMap = new HashMap<Character, BaseOperation>();
    private HashMap<String, BaseFunction> funcMap = new HashMap<String, BaseFunction>();
    private HashMap<String, BigDecimal> variableMap = new HashMap<String, BigDecimal>();

    public Calculator() {
        fsc = new FormulaScanner(LEFT_PAR, RIGHT_PAR, FUNC_PARAMS_SEP);
        oprMap.put(LEFT_PAR, new NullOperation(LEFT_PAR));
        oprMap.put(RIGHT_PAR, new NullOperation(RIGHT_PAR));
        oprMap.put(FUNC_PARAMS_SEP, new NullOperation(FUNC_PARAMS_SEP));
    }

    private BigDecimal parseNumber(String num) {
        return new BigDecimal(num);
    }

    public BigDecimal calculation(String formula) throws ParseException {
        return calculation(formula, 35);
    }

    public BigDecimal calculation(String formula, int scale) throws ParseException {
        fsc.fromString(formula);
        FormulaScanner.FormulaPart res;
        while(fsc.hasNext()) {
            res = fsc.next();
            switch (res.getType()) {
                case NUMBER:
                    numStack.push(parseNumber(res.toString()));
                    break;
                case SYMBOL:
                    if(res.toString().equals(leftPar)) {
                        oprStack.push(oprMap.get(LEFT_PAR));
                    } else if(res.toString().equals(funcParSep)) {
                        oprStack.push(oprMap.get(FUNC_PARAMS_SEP));
                    } else if(res.toString().equals(rightPar)) {
                        Executable opr;
                        ArrayList<BigDecimal> paramsArray = new ArrayList<BigDecimal>();
                        while (!(opr = oprStack.pop()).toString().equals(leftPar)
                                && !(opr instanceof BaseFunction)) {

                            if(!opr.toString().equals(funcParSep)) {
                                BigDecimal num1 = numStack.pop(), num2 = numStack.pop();
                                BigDecimal result = opr.exec(num2, num1);
                                numStack.push(result);
                            } else {
                                paramsArray.add(numStack.pop());
                            }
                        }
                        if(opr instanceof BaseFunction) {
                            paramsArray.add(numStack.pop());
                            BigDecimal result = opr.exec(paramsArray.toArray(new BigDecimal[0]));
                            numStack.push(result);
                        }
                    }
                    break;
                case OPERATOR:
                    BaseOperation opr = oprMap.get(res.toString().charAt(0));
                    Executable top;
                    while (!oprStack.empty()
                            && !(top = oprStack.peek()).toString().equals(leftPar)
                            && !(top instanceof BaseFunction)
                            && !(top.toString().equals(funcParSep))
                            && top.getPriority() >= opr.getPriority()) {

                        Executable cOpr = oprStack.pop();
                        BigDecimal num1 = numStack.pop(), num2 = numStack.pop();
                        BigDecimal result = cOpr.exec(num2, num1);
                        numStack.push(result);
                    }
                    oprStack.push(opr);
                    break;
                case VARIABLE:
                    BigDecimal value = variableMap.get(res.toString());
                    numStack.push(value);
                    break;
                case FUNCTION:
                    oprStack.push(funcMap.get(res.toString()));
                    break;
            }
        }
        while (!oprStack.empty()) {
            Executable opr = oprStack.pop();
            BigDecimal num1, num2;
            try {
                num1 = numStack.pop();
                num2 = numStack.pop();
            } catch (EmptyStackException e) {
                throw new ParseException("操作符过多", 0);
            }
            BigDecimal result = opr.exec(num2, num1);
            numStack.push(result);
        }
        if(numStack.size() != 1) throw new ParseException("操作符过少", 0);
        return BigDecimalMath.formatDecimal(numStack.pop(), scale);
    }

    public void addOperation(BaseOperation oprObj) {
        char oprName = oprObj.getOpr();
        oprMap.put(oprName, oprObj);
        fsc.addOperation(oprName);
    }

    public void removeOperation(char oprName) {
        oprMap.remove(oprName);
        fsc.removeOperation(oprName);
    }

    public void removeOperation(BaseOperation opr) {
        char oprName = opr.getOpr();
        oprMap.remove(oprName);
        fsc.removeOperation(oprName);
    }

    public void addFunction(BaseFunction func) {
        String funcName = func.getFuncName();
        funcMap.put(funcName, func);
        fsc.addFunction(funcName);
    }

    public void removeFunction(String funcName) {
        funcMap.remove(funcName);
        fsc.removeFunction(funcName);
    }

    public void removeFunction(BaseFunction func) {
        String name = func.getFuncName();
        funcMap.remove(name);
        fsc.removeFunction(name);
    }

    public void setVariable(String name, double value) {
        variableMap.put(name, new BigDecimal(value));
        fsc.addVariable(name);
    }

    public void setVariable(String name, String value) {
        variableMap.put(name, new BigDecimal(value));
        fsc.addVariable(name);
    }

    public void deleteVariable(String name) {
        variableMap.remove(name);
        fsc.removeVariable(name);
    }

    public static Calculator createCalc() {
        Calculator calc = new Calculator();
        calc.addOperation(new AddOperation());
        calc.addOperation(new SubOperation());
        calc.addOperation(new ProdOperation());
        calc.addOperation(new DivOperation());
        calc.addOperation(new BaseOperation('%', 2) {
            @Override
            public BigDecimal operation(BigDecimal a, BigDecimal b) {
                return a.divideAndRemainder(b)[1];
            }
        });
        calc.addOperation(new BaseOperation('^', 3) {
            @Override
            public BigDecimal operation(BigDecimal a, BigDecimal b) {
                // XXX: 存在误差
                double n1 = a.doubleValue();
                double n2 = b.doubleValue();
                return new BigDecimal(String.valueOf(Math.pow(n1, n2)));
            }
        });

        calc.addFunction(new SqrtFunction());
        calc.addFunction(new BaseFunction("abs", 1) {
            @Override
            public BigDecimal function(BigDecimal... v) {
                return v[0].abs();
            }
        });
        calc.addFunction(new BaseFunction("sum", 1, Integer.MAX_VALUE) {
            @Override
            public BigDecimal function(BigDecimal... v) {
                BigDecimal result = new BigDecimal(0);
                for(BigDecimal each: v) result = result.add(each);
                return result;
            }
        });
        calc.addFunction(new BaseFunction("square", 1) {
            @Override
            public BigDecimal function(BigDecimal... v) {
                return v[0].pow(2);
            }
        });

        calc.setVariable("PI", String.valueOf(Math.PI));
        calc.setVariable("e", String.valueOf(Math.E));
        calc.setVariable("WFJ", 6666);

        return calc;
    }

    public static void main(String[] args) throws ParseException {
        Calculator calc = createCalc();
        BigDecimal num = calc.calculation("12*(27+3*(3-1+3*(5/2)*PI-(12+22)/23+22))-120/8+PI-sqrt(2)");
        System.out.println(num);
    }
}
